/***********************************************************
 flabbybox - Music Player Client
 by Andrew Robinson <flabby@softhome.net>
 based on epmcd
 by Jeroen Massar <jeroen@unfix.org>
************************************************************

 $Author: flabby $
 $Id: $
 $Date: $
***********************************************************/

#include "flabbybox.h"
#include "menu.h"
#include "UsbLcd.h"

#include <usb.h>
#include <errno.h>

#define _GNU_SOURCE

#define flabbybox_VERSION "0.12.1"
#define flabbybox_VSTRING "flabbybox %s by Andrew Robinson <flabby@softhome.net>\n"

usb_dev_handle *dh;

struct flabbybox_events	events[100];
unsigned int	maxevent = 0;

unsigned int	drop_uid = 0, drop_gid = 0;

struct menu *flabbybox_menu=NULL, *current_menu=NULL;

#define MaxStringLength 200
#define DisplayWidth 20
#define BrowseTime 20;

int scroll_pointer=0, DisplayMode = 0;

char line0[MaxStringLength+1];
char line1[MaxStringLength+1];
char line2[MaxStringLength+1];
char line3[MaxStringLength+1];



void dologA(int level, const char *fmt, va_list ap)
{
	char buf[8192];

	vsnprintf(buf, sizeof(buf), fmt, ap);

	{
		FILE *out = (level == LOG_DEBUG || level == LOG_ERR ? stderr : stdout);
		fprintf(out, "[%6s] ",
			level == LOG_DEBUG ?    "debug" :
			(level == LOG_ERR ?     "error" :
			(level == LOG_WARNING ? "warn" :
			(level == LOG_NOTICE ?  "notice" :
			(level == LOG_INFO ?    "info" : "(!?)")))));
		fprintf(out, buf);
	}
}

void dolog(int level, const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	dologA(level, fmt, ap);
	va_end(ap);
}


/* Find USB IR device and open device handle.
 */
static usb_dev_handle *find_dev()
{

/* Default USB device
 */
#define USB_VENDOR  0x147a
#define USB_PRODUCT 0xe019

  struct usb_bus *busses, *bus;
  usb_dev_handle *dh;
  int e;

  usb_init();
  usb_find_busses();
  usb_find_devices();
    
  busses = usb_get_busses();
  if (busses == 0) {
    dolog(LOG_ERR, "usblib: nothing there!\n");
    return 0;
  }

  for (bus = busses; bus; bus = bus->next) {
    struct usb_device *dev;
    
    for (dev = bus->devices; dev; dev = dev->next) {
      if (dev->descriptor.idVendor == USB_VENDOR &&
	  dev->descriptor.idProduct == USB_PRODUCT) {

	int c, i, a;
	char driver[20];

        dolog(LOG_DEBUG, "USB device found.\n");

	if (geteuid() != 0)
	  dolog(LOG_WARNING, "Warning: normally, only root can program USB devices.\n");

	/* Only one configuration, interface and alt. setting */
	c = 1;
	i = a = 0;

	/* Open the device,
	   set the alternate setting,
	   and claim the interface */
	dh = usb_open(dev);

	e = usb_get_driver_np(dh, 0, driver, 20);
	if (e == -ENODATA) {
	    dolog(LOG_DEBUG, "OK, device interface not in use.\n");
	} else if (e < 0) {
	  dolog(LOG_ERR, "Can't find current driver: %s\n", strerror(-e));
	  return 0;
	} else {
	  dolog(LOG_DEBUG,
		    "Interface claimed by '%s', trying to detach\n", driver);
	  e = usb_detach_kernel_driver_np(dh, i);
	  if (e < 0) {
	    dolog(LOG_ERR, "Can't detach interface: %s\n", strerror(-e));
	    return 0;
	  }
	}

	e = usb_set_configuration(dh, 1);
	if (e < 0) {
	  dolog(LOG_ERR, "Can't set configuration: %s\n", strerror(-e));
	  return 0;
	}

	e = usb_claim_interface(dh, i);
	if (e < 0) {
	  dolog(LOG_ERR, "Can't claim interface: %s\n", strerror(-e));
	  return 0;
	}
	dolog(LOG_DEBUG, "Claimed USB device interface.\n");

	e = usb_set_altinterface(dh, a);
	if (e < 0) {
	  dolog(LOG_ERR, "Can't set altinterface: %s\n", strerror(-e));
	  return 0;
	}

	return dh;
      }
    }
  }

  dolog(LOG_ERR, "Sorry, can't find device.\n");
  return 0;
}



int read_device(usb_dev_handle *dev)
{
  int timeout = 200;
  unsigned char x[4];

/* The device sends 4 byte packets.
 *  {0x00 0xf7 KeyNum ~KeyNum}
 *
 * Zol100: key numbers range from 0x00 to 0x0d (14 keys)
 */
  
    int e = usb_interrupt_read(dev, 1, (char *)x, 4, timeout);

/*    dolog(LOG_DEBUG, "usb_interrupt_read returned %d\n", e); */


    if (e < 0) {
      if (e == -ETIMEDOUT) {
/*	dolog(LOG_DEBUG, "Repeat timeout\n"); */
      }
      else {
	dolog(LOG_ERR, "Interrupt read: %s\n", strerror(-e));
      } 
    }
    else if (e == 4) {
      dolog(LOG_DEBUG, "USB message: {%02x %02x %02x %02x}.\n", x[0], x[1], x[2], x[3]);
      e = x[2];
      }
    else {
      dolog(LOG_DEBUG, "Short read: %d bytes.\n", e);
      e = 0;
    }

    return e;
  
}


int read_key()

{
int e;
int keys[] = {
KEY_POWER, KEY_UP, KEY_BACK, KEY_LEFT, KEY_ENTER, KEY_RIGHT, KEY_PREVIOUSSONG,
KEY_DOWN, KEY_NEXTSONG, KEY_STOP, KEY_PLAYPAUSE, KEY_VOLUMEUP, KEY_MENU, KEY_VOLUMEDOWN
}; 

	e=read_device(dh);
/*    dolog(LOG_DEBUG, "read_device returned %d\n", e);*/


	if (e < 0) {
	return e;
	}
	else {
	return keys[e];
	}

}


void f_next(char *arg)
{
	dolog(LOG_INFO,"f_next called\n");
}

void f_prev(char *arg)
{
	dolog(LOG_INFO,"f_prev called\n");
}

void f_stop(char *arg)
{
	dolog(LOG_INFO,"f_stop called\n");
}

void f_play(char *arg)
{
	dolog(LOG_INFO,"f_play called\n");
}


void f_browse(char *arg)
{
	dolog(LOG_INFO,"f_browse called\n");
/*
display the menu
set the display mode
*/
	display_menu(current_menu);
}

void f_browsestep(char *arg)
{

int i, steps, dir=1;
struct menu_entry *p;


	dolog(LOG_INFO,"f_browsestep %s called\n", arg);

	steps = atoi(&arg[0]);
	if (steps<0) dir = -1;
	steps = steps * dir;

	for (i = 0; i<steps; i++)
	{
		if (dir<0) {
		 	p = current_menu->pointer->down;
		}
		else {
			p = current_menu->pointer->up;
		}
		if (p != NULL) current_menu->pointer = p;
	}

	display_menu(current_menu);

}

void f_browsesel(char *arg)
{
/*
take the action 
set the display mode
*/
	dolog(LOG_INFO,"f_browsesel called\n");
	current_menu->pointer->action(current_menu->pointer->args);
}

void f_gotomenu(char *arg)
{

	struct menu *p;

	p = find_menu(flabbybox_menu, arg);
	p->back = current_menu;
	current_menu=p;
	current_menu->pointer = current_menu->entries;
	display_menu(current_menu);
}

void f_prevmenu(char *arg)
{

	current_menu = current_menu->back ? current_menu->back : current_menu;

	display_menu(current_menu);
}


struct flabbybox_funcs func_map[] =
{
	{"MPD_NEXT",	f_next,		"mpd_next",			"MPD Next Track"},
	{"MPD_PREV",	f_prev,		"mpd_prev",			"MPD Previous Track"},
	{"MPD_STOP",	f_stop,		"mpd_stop",			"MPD Stop Playing"},
	{"MPD_PLAY",	f_play,		"mpd_play",			"MPD Start Playing"},
	{"BROWSE",	f_browse,	"browse",			"BROWSE Start Browsing"},
	{"BROWSE_STEP",	f_browsestep,	"browse_step",			"BROWSE Move Selection (+|-) steps"},
	{"GOTO_MENU",	f_gotomenu,	"goto_menu",			"GOTO Menu <menu>"},
	{"PREV_MENU",	f_prevmenu,	"prev_menu",			"GOTO Previous menu"},
	{"BROWSE_SEL",	f_browsesel,	"browse_sel",			"BROWSE Select"},
	{NULL,		NULL,		NULL,					"undefined"}
};


/********************************************************************/

bool set_event(uint16_t type, uint16_t code, int32_t value, void (*action)(char *arg), char *args)
{
	if (maxevent >= (sizeof(events)/sizeof(events[0])))
	{
		dolog(LOG_ERR, "Maximum number of events reached\n");
		return false;
	}

	events[maxevent].type = type;
	events[maxevent].code = code;
	events[maxevent].value = value;
	events[maxevent].action = action;
	events[maxevent].args = args ? strdup(args) : args;

	maxevent++;
	return true;
}

/*
	KEY_KPSLASH RELEASE f_seek -1
	<key> <value> <action> <arg>
*/

bool set_event_from_map(char *buf, struct flabbybox_mapping *event_map, struct flabbybox_mapping *value_map)
{
	unsigned int i, o = 0, len = strlen(buf), l, event = 0, value = 0, func = 0;
	void (*what)(char *arg);
	char *arg = NULL;

	for (i=0; event_map[i].code != flabbybox_MAPPING_END; i++)
	{
		l = strlen(event_map[i].name);
		if (len < o+l || buf[o+l] != ' ') continue;
		if (strncasecmp(&buf[o], event_map[i].name, l) == 0) break;
	}

	if (event_map[i].code == flabbybox_MAPPING_END)
	{
		dolog(LOG_DEBUG, "Undefined Code at %u in '%s'\n", o, buf);
		return false;
	}
	event = i;

	o += l+1;
	for (i=0; value_map[i].code != flabbybox_MAPPING_END; i++)
	{
		l = strlen(value_map[i].name);
		if (len < o+l || buf[o+l] != ' ') continue;
		if (strncasecmp(&buf[o], value_map[i].name, l) == 0) break;
	}

	if (value_map[i].code == flabbybox_MAPPING_END)
	{
		dolog(LOG_DEBUG, "Undefined Key Value at %u in '%s'\n", o, buf);
		return false;
	}
	value = i;

	o += l+1;
	for (i=0; func_map[i].name != NULL; i++)
	{
		l = strlen(func_map[i].name);
		if (len != o+l && (len < o+l || buf[o+l] != ' ')) continue;
		if (strncasecmp(&buf[o], func_map[i].name, l) == 0) break;
	}

	if (func_map[i].name == NULL)
	{
		dolog(LOG_DEBUG, "Undefined Function at %u in '%s'\n", o, buf);
		return false;
	}
	func = i;

	o += l+1;
	if (len > o) arg = &buf[o];

	dolog(LOG_DEBUG, "Mapping Event %s (%s) %s (%s) to do %s (%s) with arg %s\n",
		event_map[event].name, event_map[event].label,
		value_map[value].name, value_map[value].label,
		func_map[func].name, func_map[func].label,
		arg ? arg : "<none>");

	return set_event(EV_KEY, event_map[event].code, value_map[value].code, func_map[func].function, arg);
}

/********************************************************************/

/*
	 0 = failed to open file
	 1 = all okay
	-1 = error parsing file
*/
int readconfig(char *cfgfile)
{
	unsigned int line = 0;

	FILE *f = fopen(cfgfile, "r");

	dolog(LOG_DEBUG, "ReadConfig(%s) = %s\n", cfgfile, f ? "ok" : "error");

	if (!f) return 0;

	while (!feof(f))
	{
		char buf[1024], buf2[1024];
		unsigned int n, i = 0, j = 0;

		line++;

		if (fgets(buf2, sizeof(buf2), f) == 0) break;
		n = strlen(buf2)-1;

		/*
		 * Trim whitespace
		 * - Translate \t to space
		 * - strip multiple whitespaces
		 * Saves handling them below
		 */
		for (i=0,j=0;i<n;i++)
		{
			if (buf2[i] == '\t') buf2[i] = ' ';
			if ((i == 0 || (i > 0 && buf2[i-1] == ' ')) &&
				(buf2[i] == ' ' || buf2[i] == '\t'))
			{
				continue;
			}
			buf[j++] = buf2[i];
		}
		/* Trim trailing space if it is there */
		if (j>0 && buf[j-1] == ' ') j--;
		/* Terminate our new constructed string */
		buf[j] = '\0';
		n = j;

		/* Empty or comment line? */
		if (	n == 0 ||
			buf[0] == '#' ||
			(buf[0] == '/' && buf[1] == '/'))
		{
			continue;
		}

		dolog(LOG_DEBUG, "%s@%04u: %s\n", cfgfile, line, buf);

		if (strncasecmp("key ", buf, 4) == 0)
		{
		   if (!set_event_from_map(&buf[4], key_event_map, key_value_map)) return -1;
		}
		else if (strncasecmp("menu ", buf, 5) == 0)
		{
			flabbybox_menu = add_menu(flabbybox_menu, &buf[5]);
		}
		else if (strncasecmp("menu-entry ", buf, 11) == 0)
		{
			extract_menu_entry(flabbybox_menu, &buf[11], func_map);
		}
		else if (strncasecmp("user ", buf, 5) == 0)
		{
			struct passwd *passwd;

			/* setuid()+setgid() to another user+group */
			passwd = getpwnam(&buf[5]);
			if (passwd)
			{
				drop_uid = passwd->pw_uid;
				drop_gid = passwd->pw_gid;
			}
			else
			{
				dolog(LOG_ERR, "Couldn't find user %s\n", optarg);
				return -1;
			}
		}
		else
		{
			dolog(LOG_ERR, "Unrecognized configuration line %u: %s\n", line, buf);
			return -1;
		}
	}

	fclose(f);

	return 1; 
}

void displayline(int row, char *line, bool scroll, bool cursor)

{
	int n, m;
	char displayline[DisplayWidth+1];

/*	dolog(LOG_DEBUG,"displayline : %d : %s\n", row, line );*/


	n = strlen(line);
	/* if it's greater than DisplayWidth long and 'scroll' is true ... */
	if ((n>DisplayWidth) & scroll) {

	 m = n - DisplayWidth;
	 m = scroll_pointer % (m+1);

	/* ... rotate by scrollpointer positions and trim to DisplayWidth */
		snprintf(displayline,DisplayWidth+1, "%s", line + m);

	}
	else /* else pad with spaces */
	{
		snprintf(displayline,DisplayWidth+1, "%-20s", line);
	}

	if (cursor) {
		displayline[0] = '>';
	}

	/* write out the line */

	UsbLcd_write_at(row,0,displayline);

	if (row==0) scroll_pointer++;

}

void updatedisplay()

{
/*	dolog(LOG_DEBUG,"Display mode = %d\n", DisplayMode);*/

}

int main (int argc, char **argv)
{
	int option_index, j , timeoutcount;
	char			*device = NULL, *cfgfile = NULL, *conffile = NULL, *t;
	struct input_event	ev, prev;
	struct flabbybox_events	*evt;
	unsigned int		i, repeat = 0;

	dolog(LOG_INFO, flabbybox_VSTRING, flabbybox_VERSION);

	flabbybox_menu = NULL;
	readconfig("/etc/flabbybox.conf");
	
	current_menu=NULL;
	f_gotomenu("TopMenu"); /*start at the top of the tree */

	/* Find USB IR device and open device handle.
	 */
        dh = find_dev(); 

	/*
	 * Initialise the LCD display  */	 
        UsbLcd_init(0xEE17, 0x0001);
 
	/*
	 * Drop our root priveleges.
	 * We don't need them anymore anyways
	 */
	if (drop_uid != 0)
	{
		dolog(LOG_INFO, "Dropping userid to %u...\n", drop_uid);
		setuid(drop_uid);
	}
	if (drop_gid != 0)
	{
		dolog(LOG_INFO, "Dropping groupid to %u...\n", drop_gid);
		setgid(drop_gid);
	}

	while (true)
	{
		
		int key_pressed;

		key_pressed = read_key(dh);

		if (key_pressed<0){
			updatedisplay();
			timeoutcount = timeoutcount - 1;
 			if (timeoutcount<0) {
			 timeoutcount=0;
			 DisplayMode =0;
                        }
 			continue;
		}

		timeoutcount = BrowseTime;

		dolog(LOG_DEBUG,"Key_pressed = %d\n", key_pressed);		

		/* Lookup the key in our table */
		i = 0;
		while (	i < maxevent && events[i].code != key_pressed )
		{
			i++;
		}

		if (i < maxevent) evt = &events[i];
		else evt = NULL;


		if (evt == NULL) continue;
		evt->action(evt->args);
	}

	dolog(LOG_INFO, "flabbybox shutting down\n");

	UsbLcd_deinit();

	return 0;
}

